package com.vdom.comms;

import java.io.EOFException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.io.OptionalDataException;
import java.io.StreamCorruptedException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;


import com.vdom.comms.Event.EType;

/**
 * Ok, I reworked this class a little.
 * Originally, this was a class for a hybrid between asynchronous and event-driven communication.
 * One could communicate using public get and put asynchronously and / or run this class as a thread.
 * 
 * Now this class spawn a thread upon creation and communicate with it in a threadsafe manner.
 * Use doWait() in place of get(); put() keeps its functionality. 
 *
 */

public class Comms {
	final static int TIMEOUT = 15000; // 15 seconds in ms
	final static boolean DEBUGGING = false;
	
	String host;
	int port;
	EventHandler parent;

	private boolean isServer = true;
    
    
    public class MonitorObject{};
    
    private Socket pclient = null;
    private SocketThread networkThread;
	
	LinkedBlockingQueue<Event> latestEvents = new LinkedBlockingQueue<Event>();

	
	/*
	 * The following functions (doWait, get_ts, poll, doWaitTimeout) are used to synchronously receive packets
	 * in a thread-safe manner (unlike the horrible mess we had before). 
	 * !!! They receive everything for which the message-handler returned false. !!!
	 */
	
	
	public Event doWait(){
		return doWaitTimeout(TIMEOUT);
	}
	
	public Event get_ts() { // the old get, but threadsafe
		return doWaitTimeout(-1);
	}
	
	public Event poll() { // Look if we received something but don't block
		return doWaitTimeout(0);
	}
	
	/**
	 * Returns the latest Event or null if none received within timeout
	 * @param timeout in milliseconds. May be 0 if we don't wait (poll), or negative for infinite wait 
	 */
	public Event doWaitTimeout(long timeout) {
		Event e = null;
		long towait = 0;
		long endtime = -1;
		boolean usetimeout = (timeout > 0);
		if (usetimeout) {
			endtime = System.nanoTime() + timeout * 1000 * 1000; // Don't use System.currentTimeMillis, it fails on timezone change etc.
		}

		if (networkThread.getDone()) // dispatch loop has finished
			return null;
		
		while(e == null){
			if (usetimeout) {
				towait = (endtime - System.nanoTime());
				if (towait <= 0) {
					break;
				}
			} else {
				if (timeout == 0) { // return instantly
					return latestEvents.poll();
				}
			}

			try{
				if (usetimeout) {
					e = latestEvents.poll(towait, TimeUnit.NANOSECONDS);
				} else {
					e = latestEvents.take(); // wait indefinitely
				}
			} catch(InterruptedException e1){
				
			}				

			if (e != null && e.t == EType.KILLSENDER) { // We need the possibility to cancel a get_ts in case we had a put_ts error.
				return null;
			}
			if (networkThread.getDone()) // dispatch loop has finished
				return null;


		}
		return e;
	}
	
	public void put_ts(Event e) {
		if (!networkThread.getDone())
			networkThread.put(e);
	}

	
	/**
	 * Wait for the created thread to finish initializing, so that we know
	 * if we want to throw an exception.
	 * TODO: This is not the android-way of doing things, but it fits better into the existing codebase.
	 */
	private void waitForThreadInit(long timeout) {
		synchronized (networkThread.exceptionMonitorObject) {
			long endtime = System.nanoTime() + timeout * 1000 * 1000;  // Don't use System.currentTimeMillis, it fails on timezone change etc.
			long towait = timeout;  // wait 2 seconds max
			while (!networkThread.socketThreadInitialized) {
				try {
					networkThread.exceptionMonitorObject.wait(towait);
				} catch (InterruptedException e) {
					// don't care really
				}
				towait = (endtime - System.nanoTime()) / (1000 * 1000);
				if (towait <= 0) {
					break;
				}
			}
		}
	}
	
	
	/**
	 * Initialize this as server.
	 * 
	 * This starts the network thread, which does the initialization
	 * of the socket and starts listening.
	 * This also waits until the network thread is done initializing and
	 * throws an error. TODO: This could block if the socket initialization
	 * takes long (does that happen?)
	 * @param parent In the example it's the VDomServer object
	 * @param port
	 * @throws IOException
	 */
	public Comms(EventHandler parent, int port) throws IOException {
		this.parent = parent;
		this.isServer = true;
		this.port = port;
		parent.debug("Creating server");
		networkThread = new SocketThread();

		new Thread(networkThread).start();

		waitForThreadInit(2000);
		
		if (networkThread.thrownIOException != null) { throw networkThread.thrownIOException; }
	}
	
	/**
	 * Initialize as client
	 * 
	 * This starts the network thread, which does the initialization
	 * of the socket and connects.
	 * TODO: Do callers rely on the socket being connected as soon
	 * as this returns?
	 * @param parent (GameActivity)
	 * @param host
	 * @param port
	 */
	public Comms(EventHandler parent, String host, int port) throws StreamCorruptedException, IOException, UnknownHostException {
		this.parent = parent;
		this.isServer = false;
		this.host = host;
		this.port = port;
		parent.debug("Creating client");
		networkThread = new SocketThread();
		
		new Thread(networkThread).start();
		
		waitForThreadInit(2000);
		
		if (networkThread.thrownUnknownHostException != null ) {throw networkThread.thrownUnknownHostException; }
		if (networkThread.thrownStreamCorruptedException != null)  {throw networkThread.thrownStreamCorruptedException; }
		if (networkThread.thrownIOException != null) { throw networkThread.thrownIOException; }
	}
	

	public String getHost() {
		return host;
	}
	public int getPort() {
		return port;
	}
	
	
	private class SocketThread implements Runnable {

		private ServerSocket pserver = null;

	    private ObjectInputStream ois = null;
	    private ObjectOutputStream oos = null;
	    
	    private volatile boolean done = false; // this is true if and only if the dispatchLoop is working

	    /**
	     * Exception can be UnknownHostException, IOException, or StreamCorruptedException 
	     */
		public IOException thrownIOException = null; // this is set to something in case creating the server fails
		public UnknownHostException thrownUnknownHostException = null;
		public StreamCorruptedException thrownStreamCorruptedException = null;
		
		public boolean socketThreadInitialized = false;
	    
		private MonitorObject exceptionMonitorObject = new MonitorObject();
		
		private SendingThread sendingThread = null;
		
		/**
		 * Wake up threads waiting for us to receive something with get or doWait
		 * @param e the event we received
		 */
		private void doNotify(Event e){
				latestEvents.offer(e);
		}
		
		/**
		 * doWait will return NULL once
		 */
		public void injectNullReceived() {
			doNotify(new Event(EType.KILLSENDER));
		}
		
		/**
		 * Set 'done' to true and wake up threads waiting for us to receive something. 
		 */
		public void setDoneTrue() {
			done = true;
			injectNullReceived();
		}
		
		public boolean getDone() {
			return done;
		}
		
		/**
		 * Check if we are currently connected
		 * @return true if connected, false otherwise
		 */
		private boolean isConnected() {
			return (pclient == null ? false : pclient.isConnected());
		}
		
		private void CreateServer() throws IOException {
			debug("Opening server socket...");

			pserver = new ServerSocket(port);
			host = pserver.getInetAddress().getHostAddress();
			debug("Opened: " + host + " / " + port);
		}
		
		public SocketThread()
		{
			
		}
		
		private void debug(String s) {
			s = host + ":" + port + " -- " + s;
			// System.err.println (":: Androminion :: " + s);
			if (DEBUGGING)
				parent.debug(s);
		}
		
		/**
		 * This function blocks until it received something from the network.
		 * This function should only be called from the network thread! 
		 * @return the received object, casted to an Event
		 * @throws IOException
		 */

		private Event get() throws IOException {
			Event p = null;
			try {
				// debug("Trying to get...");
				p = (Event) ois.readObject();
				debug("Got: " + p.toString());
			} catch (OptionalDataException e) {
				debug("OptionalDataException in Comms.get() -- ignoring.");
			} catch (ClassNotFoundException e) {
				debug("ClassNotFoundException in Comms.get() -- ignoring.");
	        } catch (NullPointerException e) {
	            debug("NullPointerException in Comms.get() -- ignoring.");
	        }
			
			return p;
		}
		
		/**
		 * This function assumes that done is set to true.
		 * @return
		 */
		private boolean disconnect() {
			if (!done) {
				debug("Comms::SocketThread: 'disconnect' executed, but 'done' is not true.");
			}
			boolean clean = true;
			debug("Shutting down...");
			
			debug("Waiting for sendqueue to drain");
			put(new Event(EType.KILLSENDER));
			while (sendingThread != null && sendingThread.isAlive()) {
				try {
					sendingThread.join();
				} catch (InterruptedException e) {
					// go on waiting
				}
			}
			
			try {
				// close I/O streams
		        pclient.shutdownInput();
		        pclient.shutdownOutput();
		        debug("Streams shutdown.");
			} catch (Exception e) {
				clean = false;
			}

			try {
				oos.close();
		        ois.close();
		        debug("Streams closed.");
			} catch (Exception e) {
				clean = false;
			}

			try {
		        // close socket connection
		        pclient.close();
		        debug("Socket closed.");
			} catch (Exception e) {
				clean = false;
			}

			if (isServer) {
		        // close server
				// debug ("Stopping server...");
	            try {
	            	pserver.close();
	            	debug("Server stopped");
	            } catch (Exception e) {
	            	clean = false;
	            }
			}
			
			ois = null;
			oos = null; 
			pclient = null;
			pserver = null;
			debug(clean? "Disconnected cleanly" : "No clean disconnect. Apparently was already partially disconnected.");
			return clean;
		}
		
		
		
		private LinkedBlockingQueue<Event> toSendQueue = new LinkedBlockingQueue<Event>();
		private class SendingThread extends Thread {
			@Override
			public void run() {
				Event toSend;
				while (true) {
					try {
						toSend = toSendQueue.take();
					} catch (InterruptedException e) {
						continue;
					}
					debug("sending event " + toSend.toString());
					if (toSend.t == EType.KILLSENDER) {
						break;
					}
					try {
						oos.writeObject(toSend);
					} catch (IOException e) {
						if (!done) { // done is volatile, so this works
							parent.sendErrorHandler(e);
						}
					}
				} // while (true)
				debug("sending thread dying");
			}
		}
		
		public void put(Event p) {
			debug("Put: " + p.toString());
			if (!toSendQueue.offer(p)) {
				debug("Send Queue is full. Since the capacity of the queue is MAX_VALUE, you will not see this.");
				System.exit(1); // TODO: let the user know in some nicer way that something is seriously broken
			}
		}
		
		private Event ping() {
			// the try-block is not necessary; if there is an error, we will notice
			// since we aren't receiving anything.
//			try {
			put(new Event(Event.EType.PING));
//			} catch (Exception e) {
//				debug("Exception in Comms.ping() while sending -- quitting.");
//				e.printStackTrace();
//				return true;
//			}
			
			Event p;
			
			try {
				p = get(); // This is legal, we are in the receiving thread
			} catch (SocketTimeoutException e) {
				debug("Timed out in Comms.ping() -- quitting.");
				return null;
			} catch (Exception e) {
				debug("Exception in Comms.ping() while recving -- quitting.");
				e.printStackTrace();
				return null;
			}

			if (p == null) {
				debug("Invalid packet in Comms.ping() -- quitting.");
			}
			return p;
		}
		

		private void connect() throws UnknownHostException, IOException, StreamCorruptedException {
			if (isConnected())
				return;

			// open a socket connection
			if (isServer)
				pclient = pserver.accept();
			else
				pclient = new Socket(host, port);
			
			// Set read timeout, double for servers than for clients
			pclient.setSoTimeout(TIMEOUT * (isServer ? 2 : 1)); 

	        // open I/O streams for objects
	        oos = new ObjectOutputStream(pclient.getOutputStream());
	        ois = new ObjectInputStream(pclient.getInputStream());
	        toSendQueue.clear();
	        sendingThread = new SendingThread();
	        sendingThread.start();
		}
		
		@Override
		public void run() {
			synchronized (exceptionMonitorObject) {
				try {
					if (isServer) {
						CreateServer();
					} else {
						try {
							connect();
						} catch (UnknownHostException e) {
							thrownUnknownHostException = e;
						} catch (StreamCorruptedException e) {
							thrownStreamCorruptedException = e;
						}
					} 
				} catch (IOException e) {
					thrownIOException = e;
				} finally {
					socketThreadInitialized = true;
					exceptionMonitorObject.notify();
				}
			}
			dispatchLoop();
		}
		
		private void dispatchLoop() {
			done = false;
			if (!isConnected()) {
				if (isServer) {
					try {
						connect();
					} catch (Exception e) {
						debug("Failed to connect in run: " + e.getMessage());
						
						setDoneTrue();
						return;
					}
				} else {
					
					setDoneTrue();
					return;
				}
			}
			
			boolean timeout, disconnect = false;
			Event p = null;
	    	
	    	while (!done) {
	    		timeout = false;
	    		if (p == null) { // if p is not null, we handle p without receiving
		    		try {
						p = get();
					} catch (SocketTimeoutException e) {
						debug("Connection timed out...");
						timeout = true;
					} catch (EOFException e) {
						debug("Socket externally closed in Comms.run() -- quitting.");
						disconnect = true;
					} catch (Exception e) {
						debug("Other exception in Comms.run() -- quitting.");
						e.printStackTrace();
						disconnect = true;
					}
		    	}

				if (done) break;
				if (p != null && p.t == EType.SLEEP) {
					try { Thread.sleep(p.i); }
					catch (InterruptedException e) {}
					p = null;
					continue;
				}	
				if ((p != null) && (p.t == Event.EType.PING))
					try {
						put(new Event(Event.EType.PONG));
					} catch (Exception e) {
						debug("Could not pong in Comms.run() -- quitting.");
						e.printStackTrace();
						disconnect = true;
					}
				else if ((p != null) && (p.t == Event.EType.GETSERVER))
					try {
						put(new Event(Event.EType.SERVER).setString(host).setInteger(port));
					} catch (Exception e) {
						debug("Could not pong server in Comms.run() -- quitting.");
						e.printStackTrace();
						disconnect = true;
					}
				else if (p != null) {
					if (!parent.handle(p))
						doNotify(p);
				}
				if (timeout) {  // If we timed out:
					p = ping(); // send a ping.
					if (p == null) { // if ping unsuccessful: disconnect
						disconnect = true;
					} else {
						if (p.t == EType.PONG) { // if it is successful and we received PONG: delete p
							p = null;			 // OTHERWISE: keep p for next round! This solves a race condition
						}
					}
				} else {
					p = null;
				}
				
				if (disconnect) {
					p = new Event(Event.EType.DISCONNECT);
					if (!parent.handle(p))
						doNotify(p);
					setDoneTrue();
				}
	    	}
	
	    	disconnect();
	    	debug("End of Comms.run()");
		}
	}
		
	public boolean stop() {
		networkThread.setDoneTrue();
		return networkThread.disconnect();
	}
	
	public void injectNullReceived() {
		
		networkThread.injectNullReceived();
	}

}
